/** @namespace Animation support. */
Jelo.Anim = function() {
    /** @private convenience */
    var JF = Jelo.Format,
        jcg = Jelo.CSS.getStyle,
        jcs = Jelo.CSS.setStyle,
        pi = function(n) {
            return parseInt(n, 10);
        };
    
    /** @private constants */
    var _ = {
        d   : 0.5, // default duration
        a   : [], // holds animation objects
        r   : [], // contains tasks that need to be removed
        t   : null, // single timer to animate everything
        i   : 20, // 20ms = 50fps, 33ms = 30fps, 50ms = 20fps
        now : function() {
            return new Date().getTime();
        }
    };
    
    /** @private Strategy pattern to determine animation behavior. */
    var S = function() {
        /** @private percentComplete */
        var pc = function() {
            var d = this.endTime - this.startTime;
            var r = 1 - ((this.endTime - _.now()) / d);
            var p = this.easing(r);
            return Math.round(p * 1000) / 1000;
        };
        return {
            Border      : function() {/* TODO */},
            Color       : function() {
                var p = pc.call(this);
                var f = this.startVal;
                var t = this.endVal;
                f = (f.substring(0, 3) === "rgb")
                    ? JF.rgbStringToArray(f)
                    : JF.hexToRGB(f);
                t = (t.substring(0, 3) === "rgb")
                    ? JF.rgbStringToArray(t)
                    : JF.hexToRGB(t);
                var v = [];
                for (var i = 0; i < 3; i++) {
                    var delta = Math.floor(t[i] - f[i]);
                    var current = Math.floor(p * delta);
                    v[i] = current + f[i];
                }
                v = "rgb(" + v[0] + "," + v[1] + "," + v[2] + ")";
                jcs(this.element, this.property, v);
            },
            ComboPx     : function() {
                var rx, getVal, x, y, v, x0, y0, p, deltaValue, currDelta, top, right, bottom, left, i;
                switch (this.property) {
                    case "backgroundPositionX" :
                        rx = /\-?[0-9]+px/;
                        if (rx.test(this.startVal) && rx.test(this.endVal)) {
                            /** @private */
                            getVal = function(val) {
                                return val.replace(/[^0-9\-]/g, "");
                            };
                            p = pc.call(this);
                            y0 = Jelo.css(this.element, "background-position-y");
                            x = [getVal(this.startVal), getVal(this.endVal)];
                            y = [getVal(y0), getVal(y0)];
                            deltaValue = [pi(x[1]) - pi(x[0]),
                                pi(y[1]) - pi(y[0])];
                            currDelta = [Math.floor(p * deltaValue[0]),
                                Math.floor(p * deltaValue[1])];
                            v = [currDelta[0] + pi(x[0]),
                                currDelta[1] + pi(y[0])];
                            jcs(this.element, "background-position", v[0] +
                                "px " + v[1] + "px");
                        }
                        break;
                    case "backgroundPositionY" :
                        rx = /\-?[0-9]+px/;
                        if (rx.test(this.startVal) && rx.test(this.endVal)) {
                            /** @private */
                            getVal = function(val) {
                                return val.replace(/[^0-9\-]/g, "");
                            };
                            p = pc.call(this);
                            x0 = Jelo.css(this.element, "background-position-x");
                            x = [getVal(x0), getVal(x0)];
                            y = [getVal(this.startVal), getVal(this.endVal)];
                            deltaValue = [pi(x[1]) - pi(x[0]),
                                pi(y[1]) - pi(y[0])];
                            currDelta = [Math.floor(p * deltaValue[0]),
                                Math.floor(p * deltaValue[1])];
                            v = [currDelta[0] + pi(x[0]),
                                currDelta[1] + pi(y[0])];
                            jcs(this.element, "background-position", v[0] +
                                "px " + v[1] + "px");
                        }
                        break;
                    case "backgroundPosition" :
                        rx = /\-?[0-9]+px \-?[0-9]+px/;
                        if (rx.test(this.startVal) && rx.test(this.endVal)) {
                            /** @private */
                            getVal = function(val, index) {
                                return val.split(" ")[index].replace(/[^0-9\-]/g, "");
                            };
                            p = pc.call(this);
                            x = [getVal(this.startVal, 0),
                                getVal(this.endVal, 0)];
                            y = [getVal(this.startVal, 1),
                                getVal(this.endVal, 1)];
                            deltaValue = [pi(x[1]) - pi(x[0]),
                                pi(y[1]) - pi(y[0])];
                            currDelta = [Math.floor(p * deltaValue[0]),
                                Math.floor(p * deltaValue[1])];
                            v = [currDelta[0] + pi(x[0]),
                                currDelta[1] + pi(y[0])];
                            jcs(this.element, this.property, v[0] + "px " +
                                v[1] + "px");
                        }
                        break;
                    default :
                        rx = /\-?[0-9]+px \-?[0-9]+px \-?[0-9]+px \-?[0-9]+px/;
                        if (rx.test(this.startVal) && rx.test(this.endVal)) {
                            /** @private */
                            getVal = function(val, index) {
                                return val.split(" ")[index].replace(/[^0-9\-]/g, "");
                            };
                            p = pc.call(this);
                            top = [getVal(this.startVal, 0),
                                getVal(this.endVal, 0)];
                            right = [getVal(this.startVal, 1),
                                getVal(this.endVal, 1)];
                            bottom = [getVal(this.startVal, 2),
                                getVal(this.endVal, 2)];
                            left = [getVal(this.startVal, 3),
                                getVal(this.endVal, 3)];
                            deltaValue = [pi(top[1]) - pi(top[0]),
                                pi(right[1]) - pi(right[0]),
                                pi(bottom[1]) - pi(bottom[0]),
                                pi(left[1]) - pi(left[0])];
                            currDelta = [];
                            for (i = 0; i < 4; i++) {
                                currDelta[i] = Math.floor(p * deltaValue[i]);
                            }
                            v = [currDelta[0] + pi(top[0]),
                                currDelta[1] + pi(right[0]),
                                currDelta[2] + pi(bottom[0]),
                                currDelta[3] + pi(left[0])];
                            jcs(this.element, this.property, v[0] + "px " +
                                v[1] + "px " + v[2] + "px " + v[3] + "px");
                        }
                }
            },
            Numerical   : function() {
                var p = pc.call(this);
                var deltaValue = parseFloat(this.endVal, 10) -
                    parseFloat(this.startVal, 10);
                var currDelta = p * deltaValue;
                var v = currDelta + parseFloat(this.startVal, 10);
                switch (this.property) {
                    case "opacity" :
                        v = parseFloat(v);
                        if (v < 0) {
                            v = 0;
                        }
                        if (v > 1) {
                            v = 1;
                        }
                        break;
                    case "zIndex" :
                        v = pi(v);
                        break;
                }
                jcs(this.element, this.property, v);
            },
            NumericalPx : function() {
                var p = pc.call(this);
                var deltaValue = pi(this.endVal) - pi(this.startVal);
                var currDelta = Math.floor(p * deltaValue);
                var v = currDelta + pi(this.startVal);
                jcs(this.element, this.property, v + this.unit);
            }
        };
    }();
    
    /** @private Strategy pattern to determine easing behavior */
    var Easing = {
        LINEAR    : function(x) { // no easing
            return x;
        },
        IN        : function(x) { // accelerate
            return Math.pow(x, 3);
        },
        OUT       : function(x) { // decelerate
            return 1 - Math.pow(1 - x, 3);
        },
        SMOOTH    : function(x) { // accelerate then decelerate
            return x < 0.5
                ? 2 * x * x
                : -2 * Math.pow(x - 1, 2) + 1;
        },
        OVERSHOOT : function(x) { // go past, then come back
            var s = 1.70158;
            return (x -= 1) * x * ((s + 1) * x + s) + 1;
        },
        SPRING    : function(x) { // repeated overshoot
            return 1 - (Math.cos(x * 4.5 * Math.PI) * Math.exp(-x * 6));
        },
        WOBBLE    : function(x) { // forward, then back, then forward
            return (-Math.cos(3 * x * Math.PI) / 2) + 0.5;
        }
    };
    
    /** @private accessible as Jelo.Anim.ate */
    var animate = function(config) {
        if (!config) {
            return;
        }
        
        if (Jelo.Valid.isArray(config.me)) {
            var cfg = config;
            Jelo.each(config.me, function() {
                cfg.me = this;
                animate(cfg);
            });
            return;
        }
        
        // validate the animation target element
        var m = config.me || false;
        if (!m) {
            throw new Error('Jelo.Anim.ate: Missing required configuration option me:HTMLElement|String');
        } else if (m.isArray) {
            m = m[0];
        } else if (typeof m === "string") {
            m = Jelo.Dom.selectNode(m);
        }
        if (!m) {
            // need to reverify after the "else" clauses
            throw new Error('Jelo.Anim.ate: Missing required configuration option me:HTMLElement|String');
        }
        
        // validate the css property to animate
        var css = config.css || false;
        if (!css || (typeof css !== "string")) {
            throw new Error('Jelo.Anim.ate: Missing required configuration option css:String');
        }
        var c = JF.hyphenatedToCamelCase(css);
        
        // validate the starting value
        var f = config.from;
        if (!f && typeof f !== "number") {
            f = jcg(m, c);
            if (!f && typeof f !== "number") {
                f = jcg(m, css);
            }
            if (!f && typeof f !== "number") {
                f = 0; // specify config.from to override this behavior
            }
        }
        if (f === "auto") {
            f = 0; // specify config.from to override this behavior
        }
        
        // validate the ending value
        var t = '' + config.to;
        if (!t && typeof t !== "number") {
            throw new Error('Jelo.Anim.ate() Missing required configuration option to:String|Number');
        }
        
        // prepare the pre- and post-animation callback functions
        var b = (typeof config.before === "function")
            ? config.before
            : Jelo.emptyFn;
        var a = (typeof config.after === "function")
            ? config.after
            : Jelo.emptyFn;
        
        // validate the animation duration
        var d = parseFloat(config.duration);
        if (isNaN(d)) {
            d = _.d;
        }
        d = Math.floor(Math.abs(d * 1000)); // convert s to ms
        
        // validate the animation easing method
        var e = config.easing || Jelo.Anim.Easing.LINEAR;
        if ((typeof e === "string") && e.length) {
            e = Jelo.Anim.Easing[e.toUpperCase()] || Jelo.Anim.Easing.LINEAR;
        } else if (typeof e != "function") {
            throw new Error("Jelo.Anim.ate: Easing must be a string such as 'linear', 'out', etc. or a function.");
        }
        
        // clean up animation conflicts, also improves performance
        var temp = [];
        for (var i = 0; i < _.a.length; i++) {
            var ti = _.a[i];
            if ((ti.element != m) ||
                (JF.hyphenatedToCamelCase(ti.property) != c)) {
                temp.push(ti);
            }
        }
        _.a = temp;
        
        // strategy pattern to get an animation function
        var animFn = function(c) {
            switch (c) {
                case "border" :
                case "borderBottom" :
                case "borderLeft" :
                case "borderRight" :
                case "borderTop" :
                    return S.Border;
                case "backgroundColor" :
                case "color" :
                    return S.Color;
                case "backgroundPositionX" :
                case "backgroundPositionY" :
                case "backgroundPosition" :
                case "margin" :
                case "padding" :
                    return S.ComboPx;
                case "lineHeight" :
                case "opacity" :
                case "zIndex" :
                    return S.Numerical;
                case "bottom" :
                case "height" :
                case "left" :
                case "marginBottom" :
                case "marginLeft" :
                case "marginRight" :
                case "marginTop" :
                case "paddingBottom" :
                case "paddingLeft" :
                case "paddingRight" :
                case "paddingTop" :
                case "right" :
                case "top" :
                case "width" :
                    return S.NumericalPx;
                default :
                    return Jelo.emptyFn;
            }
        }(c);
        
        // build the actual animation object
        var animObj = {
            // for convenience in before and after functions
            me        : config.me,
            css       : config.css,
            duration  : config.duration,
            before    : config.before,
            after     : config.after,
            from      : config.from,
            to        : config.to,
            
            // used internally
            element   : m,
            property  : c,
            startVal  : f,
            endVal    : t,
            callback  : a,
            startTime : _.now(),
            endTime   : _.now() + d,
            fn        : animFn,
            easing    : e,
            unit      : function() {
                var unit = t.replace(/(\-?[0-9]+)(.*)?/, '$2');
                return unit.length
                    ? unit
                    : 'px';
            }()
        };
        _.a.push(animObj);
        
        b.call(animObj);
        run();
    };
    
    /** @private Handles each frame of animation. */
    var run = function() {
        var /* counter */i, /* callbacks */c, /* r[i] */ri, /* t[i] */ti, /* value */v, /* timer */t;
        if (!_.t) {
            _.t = setInterval(function() {
                if (_.r.length) {
                    c = [];
                    for (i = _.r.length - 1; i >= 0; i--) {
                        ri = _.r[i];
                        ti = _.a[ri];
                        v = null;
                        switch (ti.fn) {
                            case S.Border :
                                v = ti.endVal; // TODO: doublecheck this
                                // assignment
                                break;
                            case S.ComboPx :
                                v = ti.endVal; // TODO: doublecheck this
                                // assignment
                                break;
                            case S.Color :
                                v = "#" + ti.endVal.replace(/#/, "");
                                break;
                            case S.NumericalPx :
                                v = pi(ti.endVal) + ti.unit;
                                break;
                            case S.Numerical :
                                v = parseFloat(ti.endVal);
                                break;
                            default :
                                v = ti.endVal;
                        }
                        jcs(ti.element, ti.property, v);
                        c.push(ti);
                        _.a.splice(ri, 1);
                    }
                    _.r = [];
                    for (i = 0; i < c.length; i++) {
                        ti = c[i];
                        ti.callback.call(ti);
                    }
                }
                if (_.a.length) {
                    for (i = 0; i < _.a.length; i++) {
                        t = _.a[i];
                        if (_.now() < t.endTime) {
                            t.fn.call(t);
                        } else {
                            _.r.push(i);
                        }
                    }
                }
            }, _.i);
        }
    };
    
    /** @scope Jelo.Anim */
    return {
        /**
         * Easing functions.
         * <ul>
         * <li>LINEAR: no easing, default</li>
         * <li>IN: accelerate (speed up)</li>
         * <li>OUT: decelerate (slow down)</li>
         * <li>SMOOTH: accelerate, then decelerate</li>
         * <li>OVERSHOOT: go past the end value, then come back. occasionally
         * causes errors in IE depending on the animated property.</li>
         * <li>SPRING: repeated overshoot around the end value</li>
         * <li>WOBBLE: forward, back, then forward again in a single duration</li>
         * </ul>
         * 
         * @type Object
         */
        Easing             : Easing,
        /**
         * Animate a CSS property of a given element.
         * 
         * @function
         * @param {Object} config A configuration object.
         * @param {HTMLElement|String} config.me Object to animate, or a CSS
         *        selector for which the first matching element will be used.
         * @param {String} config.css Property to animate.
         * @param {String|Number} [config.from] Starting value for the
         *        animation.
         * @param config.to {String|Number} Ending value for the animation
         * @param {Function} [config.before] Method to invoke immediately before
         *        the animation starts.
         * @param {Function} [config.after] Method to invoke immediately after
         *        the animation finishes.
         * @param {Number} [config.duration=0.5] How many seconds the animation
         *        should last.
         * @param {Function|String} [config.easing="linear"] How to calculate
         *        property values. If a string is supplied, it must match a
         *        Jelo.Anim.Easing property name (case insensitive).
         */
        ate                : animate,
        /**
         * @function
         * @return {Boolean} True if there are currently animations in the
         *         queue.
         */
        ating              : function() {
            return !!_.a.length;
        },
        /**
         * Immediately halts and cancels all pending animations. References are
         * not stored, the animations are gone.
         */
        stopAll            : function() {
            _.r = [];
            _.a = [];
            if (_.t) {
                clearTimeout(_.t);
                _.t = null;
            }
        },
        /**
         * Changes the default animation duration, used whenever the duration
         * property is not explicitly set.
         * 
         * @param {Number} Seconds, as a whole or decimal number.
         */
        setDefaultDuration : function(d) {
            if (Jelo.Valid.isNumber(d)) {
                _.d = d;
            }
        }
    };
}();
